#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/3/27 22:47
# @Author  : Grayson Liu
# @Email   : graysonliu@foxmail.com
# @File    : ProjectForm.py

from PyQt5.Qt import *

from ui.Ui_ProjectForm import Ui_ProjectForm
from CustomFigureCanvas import CustomFigureCanvas

from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import numpy as np
import threading

from CustomMethodListWidget import CustomMethodListWidget
from MethodWizardDialog import MethodWizardDialog
from GraphicInputWizardDialog import GraphicInputWizardDialog
from GraphicOutputWizardDialog import GraphicOutputWizardDialog
from GraphicPreProcessWizardDialog import GraphicPreProcessWizardDialog
from CalculateAccuracyDialog import CalculateAccuracyDialog

from MethodList import MethodList

from GeoGraphicProcess import GeoGraphicProcess
from CustomSignal import CustomSignal


class ProjectForm(QWidget, Ui_ProjectForm):
    global_signal = CustomSignal()

    def __init__(self, parent=None):
        super(ProjectForm, self).__init__(parent)
        self.setupUi(self)

        # 成员变量
        self.method_list = MethodList()  # 方法列表
        self.set_input_flag = False  # 是否已设置输入
        self.set_output_flag = False  # 是否已设置输出
        self.need_run = True  # 是否点击”运行“/”返回方法编辑“时需要执行一遍自定义方法
        self.single_flag = False  # 是否为单步运行
        self.single_step = -1  # 如果是单步运行，进行到第几步(下标从0开始)，初始情况下-1表示没进行预处理，预处理后变成0
        self.single_result: GeoGraphicProcess = GeoGraphicProcess()  # 如果是单步运行，保留中间结果对象

        # 将ProjectForm UI中 右侧Widget设定为堆叠布局，根据选择的功能进行切换，显示不同的Widget
        self.replaceableWidget = QStackedLayout(self.functionView)

        # 在这里实例化不同功能Widget的class，使用self.replaceableWidget.addWidget(...)添加到布局中
        # 序号0，方法列表+(输入输出信息+预处理信息)显示，这部分由于是可变的，所以不能通过Qt Designer而只能手写
        information_widget = QWidget()  # 方法列表+(输入输出信息+预处理信息)的Widget
        information_widget_layout = QVBoxLayout(information_widget)  # 垂直布局，比例1:2

        sub_information_widget = QWidget()  # (输入输出信息+预处理信息)的Widget
        sub_information_widget_layout = QHBoxLayout(sub_information_widget)  # 水平布局，比例2:1

        input_output_group = QGroupBox("输入输出信息")  # 输入输出信息的Widget，是GroupBox
        input_output_group_layout = QVBoxLayout(input_output_group)
        self.txtInputOutput = QTextBrowser()
        self.txtInputOutput.setFont(QFont("宋体", 8))
        input_output_group_layout.addWidget(self.txtInputOutput)
        sub_information_widget_layout.addWidget(input_output_group)  # 设置完成后，添加到sub_information_widget

        preprocess_group = QGroupBox("预处理信息")  # 预处理信息的Widget，是GroupBox
        preprocess_group_layout = QVBoxLayout(preprocess_group)
        self.txtPreProcess = QTextBrowser()
        self.txtPreProcess.setFont(QFont("宋体", 8))
        preprocess_group_layout.addWidget(self.txtPreProcess)
        sub_information_widget_layout.addWidget(preprocess_group)  # 设置完成后，添加到sub_information_widget

        sub_information_widget_layout.setStretch(0, 2)  # 设置比例
        sub_information_widget_layout.setStretch(1, 1)
        sub_information_widget_layout.setSpacing(0)
        sub_information_widget_layout.setContentsMargins(0, 0, 0, 0)

        information_widget_layout.addWidget(sub_information_widget)  # 设置完成后，添加到information_widget

        method_list_group = QGroupBox("当前方法列表")  # 当前方法列表的Widget，是GroupBox
        method_list_group_layout = QVBoxLayout(method_list_group)
        self.customMethodList = CustomMethodListWidget()
        method_list_group_layout.addWidget(self.customMethodList)

        information_widget_layout.addWidget(method_list_group)  # 设置完成后，添加到information_widget

        information_widget_layout.setStretch(0, 1)  # 设置比例
        information_widget_layout.setStretch(1, 2)
        information_widget_layout.setSpacing(0)
        information_widget_layout.setContentsMargins(0, 0, 0, 0)

        self.replaceableWidget.addWidget(information_widget)  # 全部设置好了之后，添加到replaceableWidget

        # 序号1，画布和对应工具栏
        self.customFig = CustomFigureCanvas()
        self.figToolbar = NavigationToolbar(self.customFig, self)  # 添加一个NavigationToolbar在FigureCanvas下方
        # 将两部分组合成一个widget，再添加到replaceableWidget中
        graphic_widget = QWidget()
        graphic_widget_layout = QVBoxLayout(graphic_widget)
        graphic_widget_layout.addWidget(self.customFig)
        graphic_widget_layout.addWidget(self.figToolbar)
        self.replaceableWidget.addWidget(graphic_widget)

        # 设定默认状态下的index
        self.replaceableWidget.setCurrentIndex(0)  # 默认是方法列表

        # 将“运行”按钮设置为不可选中状态，只有当正确设置输入输出之后才解锁“运行”
        self.btnRun.setEnabled(False)
        self.btnSingleStepRun.setEnabled(False)

        # 将“精度计算”和“返回方法编辑”按钮设置为不可见，点击“运行”或者“单步运行”后才可见
        self.btnCalculate.hide()
        self.btnCalculate.setEnabled(False)
        self.btnBackMethodList.hide()

        # 绑定信号和槽
        self.btnAddMethod.clicked.connect(self.show_method_wizard)  # 打开添加方法向导
        self.btnSetInput.clicked.connect(self.show_input_wizard)  # 打开设置遥感影像输入向导
        self.btnSetOutput.clicked.connect(self.show_output_wizard)  # 打开设置输出向导
        self.btnPreProcess.clicked.connect(self.show_preprocess_wizard)  # 打开设置预处理向导

        self.btnRun.clicked.connect(self.run_method)  # 进行方法的运行
        self.btnSingleStepRun.clicked.connect(self.run_method_single)  # 进行方法的单步运行

        self.btnCalculate.clicked.connect(self.show_calculate_dialog)  # 显示精度计算对话框
        self.btnBackMethodList.clicked.connect(self.__back_method_list)  # 返回方法编辑界面

        # 绑定自定义信号和槽
        # 接收来自添加方法向导窗体MethodWizardDialog的信号，用户设定完成了方法信息后发送给ProjectForm，
        # 在成员变量method_list中添加该方法，根据信息在CustomMethodListWidget中添加一行
        MethodWizardDialog.global_signal.add_method.connect(self.append_method_list)
        # 接收来自CustomMethodListWidget中删除按钮发出的信号，用户点击删除按钮后，发送方法的index
        # 在成员变量method_list中删除该方法，根据index在CustomMethodListWidget中删除该行
        CustomMethodListWidget.global_signal.delete_method.connect(self.delete_method_list)
        # 接收来自编辑遥感影像输入窗体GraphicInputWizardDialog的信号，用户设定完成了输入信息后发送给ProjectForm，
        # 在成员变量method_list中进行设置，根据信息在当前窗体的txtInputOutput中显示信息
        GraphicInputWizardDialog.global_signal.set_input.connect(self.set_input)
        # 接收来自编辑遥感影像输入窗体GraphicOutputWizardDialog的信号，用户设定完成了输出信息后发送给ProjectForm，
        # 在成员变量method_list中进行设置，根据信息在当前窗体的txtInputOutput中显示信息
        GraphicOutputWizardDialog.global_signal.set_output.connect(self.set_output)
        # 接收来自编辑遥感影像输入窗体GraphicPreProcessWizardDialog的信号，用户设定完成了预处理信息后发送给ProjectForm，
        # 在成员变量method_list中进行设置，根据信息在当前窗体的txtPreProcess中显示信息
        GraphicPreProcessWizardDialog.global_signal.set_preprocess.connect(self.set_preprocess)

        ProjectForm.global_signal.draw_picture.connect(self.show_picture)
        ProjectForm.global_signal.update_text.connect(self.show_status)

        GeoGraphicProcess.global_signal.update_text.connect(self.show_status)

    def get_method_list(self):
        """
        获取成员变量method_list
        :return:
        """
        return self.method_list

    def init_method_list(self, project_name: str, project_path: str):
        """
        初始化成员变量method_list，用于响应”新建工程“
        :param project_name: 项目名
        :param project_path: 项目路径
        :return:
        """
        self.method_list.set_project_name(project_name)  # 设置工程名
        self.method_list.set_project_path(project_path)  # 设置工程路径
        self.method_list.save_project(project_path)  # 保存这个空的工程文件

        # 判断是否已经设置了输入输出，如果已经设置了，解锁”运行“按钮
        self.__show_input_output()
        self.__show_preprocess()

        # 将工程名显示在ProjectForm上
        self.lblCurrentProject.setText(project_name)

        # 在ProjectForm的Console上显示信息
        self.show_message("成功创建工程文件 {}".format(project_path))

    def set_method_list(self, method_list: MethodList):
        """
        设定成员变量method_list，用于响应”打开工程“
        :param method_list:
        :return:
        """
        self.method_list = method_list  # 从main函数中传递而来，是当前需要处理的工程方法列表MethodList对象

        # 读取工程文件时，应当在ProjectForm的MethodListWidget中，将读取的工程文件信息显示出来
        # 显示前，先清空当前列表里面的内容
        self.customMethodList.clear_list_item()
        # 仅方法数量不为0的时候遍历
        if method_list.project_data["method_num"] != 0:
            # 遍历方法列表
            for method in method_list.project_data["methods"]:
                # 如果该方法无参数
                if not method["parameters"]:
                    self.customMethodList.add_list_item(method["name"], "None")
                # 如果该方法有参数
                else:
                    parameters_str = ""
                    for parameter in method["parameters"]:
                        parameters_str += "{} = {} ".format(parameter["name"], parameter["value"])
                    self.customMethodList.add_list_item(method["name"], parameters_str)

        # 判断是否已经设置了输入输出，如果已经设置了，解锁”运行“按钮
        self.__show_input_output()
        self.__show_preprocess()

        # 将工程名显示在ProjectForm上
        self.lblCurrentProject.setText(method_list.project_data["name"])

        # 在ProjectForm的Console上显示信息
        self.show_message("成功打开工程文件 {}".format(method_list.project_data["path"]))

    def show_method_wizard(self):
        """
        弹出添加方法向导对话框
        :return:
        """
        dialog = MethodWizardDialog(parent=self)
        dialog.show()

    def show_input_wizard(self):
        """
        弹出设置遥感影像输入向导对话框
        :return:
        """
        dialog = GraphicInputWizardDialog(input_dic=self.method_list.project_data["input"], parent=self)
        dialog.show()

    def show_preprocess_wizard(self):
        """
        弹出设置遥感影像预处理向导对话框
        :return:
        """
        dialog = GraphicPreProcessWizardDialog(preprocess_dic=self.method_list.project_data["preprocess"], parent=self)
        dialog.show()

    def show_output_wizard(self):
        """
        弹出设置遥感影像输出向导对话框
        :return:
        """
        dialog = GraphicOutputWizardDialog(output_dic=self.method_list.project_data["output"], parent=self)
        dialog.show()

    def show_calculate_dialog(self):
        """
        弹出精度计算对话框
        :return:
        """
        dialog = CalculateAccuracyDialog(current_data=self.single_result.current_data, parent=self)
        dialog.show()

    def append_method_list(self, method_name: str, type_index: int, method_index: int, parameters: list):
        """
        自定义信号add_method的槽函数，用于在customMethodList中添加一行
        :return:
        """
        parameters_str = ""
        if not parameters:
            parameters_str = "None"
        else:
            for item in parameters:
                parameters_str += "{} = {} ".format(item["name"], item["value"])
        self.customMethodList.add_list_item(method_name, parameters_str)
        # 在method_list中添加该方法
        self.method_list.add_method(method_name, type_index, method_index, parameters)

    def delete_method_list(self, index: int):
        """
        自定义信号delete_method的槽函数，用于在customMethodList中删除一行
        :param index:
        :return:
        """
        # 在method_list中删除该方法
        self.method_list.delete_method(index)

    def show_message(self, message: str):
        """
        用于在ProjectForm的Console中添加一行
        :param message: 需要添加到Console最后一行的字符串
        :return:
        """
        self.txtConsole.append(message)
        self.txtConsole.ensureCursorVisible()  # 保持光标一直可见，即保持在最后一行

    def set_input(self, input_dic: dict):
        """
        自定义信号set_input的槽函数，用于设置method_list的输入，同时信息显示在窗体上
        :param input_dic:
        :return:
        """
        self.method_list.project_data["input"] = input_dic
        self.__show_input_output()

    def set_output(self, output_dic: dict):
        """
        自定义信号set_output的槽函数，用于设置method_list的输出，同时信息显示在窗体上
        :param output_dic:
        :return:
        """
        self.method_list.project_data["output"] = output_dic
        self.__show_input_output()

    def set_preprocess(self, preprocess_dic: dict):
        """
        自定义信号set_preprocess的槽函数，用于设置method_list的预处理，同时信息显示在窗体上
        :param preprocess_dic:
        :return:
        """
        self.method_list.project_data["preprocess"] = preprocess_dic
        self.__show_preprocess()

    def __show_input_output(self):
        """
        私有方法，根据当前method_list的内容，更新txtInputOutput的显示，同时如果符合要求就解锁”运行“按钮
        :return:
        """
        input_dic = self.method_list.project_data["input"]
        if not input_dic:  # 如果input是空的
            input_str = "未设置输入<br>"
        else:
            input_str = "<b>输入</b>：<br>路径：{}<br>类型：{}<br>波段：{}".format(input_dic["path"],
                                                                       input_dic["type"],
                                                                       input_dic["raster"])
            self.set_input_flag = True
        output_dic = self.method_list.project_data["output"]
        if not output_dic:  # 如果output是空的
            output_str = "未设置输入<br>"
        else:
            output_str = "<b>输出：</b><br>路径：{}<br>类型：{}".format(output_dic["path"],
                                                               output_dic["type"])
            self.set_output_flag = True
        if self.set_input_flag and self.set_output_flag:  # 当输入输出同时设置好了之后，解锁”运行“按钮
            self.btnRun.setEnabled(True)
            self.btnSingleStepRun.setEnabled(True)
        self.txtInputOutput.setText(input_str + "<hr>" + output_str)

    def __show_preprocess(self):
        """
        私有方法，根据当前method_list的内容，更新txtPreProcess的显示
        :return:
        """
        preprocess_dic = self.method_list.project_data["preprocess"]
        nan_str = "<b>缺失值处理方法：</b><br>"
        if preprocess_dic["nan_process"] == "none":
            nan_str += "不处理"
        elif preprocess_dic["nan_process"] == "zero":
            nan_str += "缺失值用0替代"
        else:
            nan_str += "缺失值用均值替代"
        anomaly_str = "<b>异常值处理方法：</b><br>"
        if preprocess_dic["anomaly_process"] == "none":
            anomaly_str += "不处理"
        else:
            anomaly_str += "3σ法则剔除异常值"
        self.txtPreProcess.setText(nan_str + "<hr>" + anomaly_str)

    def save_project(self):
        """
        保存当前工程，用于响应”保存“
        :return:
        """
        path = self.method_list.project_data["path"]
        self.method_list.save_project(path)
        self.show_message("工程文件已保存 {}".format(path))

    def saveas_project(self):
        """
        另存为当前工程，用于响应”保存“
        :return:
        """
        # 打开文件浏览器，新建工程文件.prj
        project_path, project_type = QFileDialog.getSaveFileName(self, "选择工程文件保存位置", filter="Project Files(*.prj)")
        if project_path == "":
            return
        self.method_list.project_data["path"] = project_path  # 另存为时，同步修改工程文件的path属性
        self.method_list.save_project(project_path)
        self.show_message("工程文件已另存为 {}".format(project_path))

    def run_method(self):
        """
        根据当前method_list的内容进行解析并运行方法
        :return:
        """

        def thread_run_methods(method_list: MethodList, geo_graphic: GeoGraphicProcess):
            """
            线程体，负责解析和执行MethodList中的方法
            :param method_list: MethodList对象
            :param geo_graphic: 传入的GeoGraphicProcess对象
            :return:
            """
            geo_graphic.read_file(path=method_list.project_data["input"]["path"])
            # 1. 根据MethodList设置的波段或者指数计算方法，生成初步的图像矩阵
            ProjectForm.global_signal.update_text.emit(
                "读取遥感影像 {}".format(method_list.project_data["input"]["path"]))
            geo_graphic.set_input_graphic(method_list.project_data["input"])

            # 2. 根据MethodList设置的预处理方法，进行预处理，得到数据清洗后的图像矩阵
            ProjectForm.global_signal.update_text.emit("执行预处理流程")
            geo_graphic.run_preprocess(method_list.project_data["preprocess"])

            # 3. 根据MethodList中的methods方法列表，处理图像数据
            ProjectForm.global_signal.update_text.emit(
                "方法步骤共计 {}".format(method_list.project_data["method_num"]))
            geo_graphic.run_methods(method_list.project_data["method_num"],
                                    method_list.project_data["methods"])
            # 4. 保存图像
            geo_graphic.save_current_graphic(method_list.project_data["output"]["path"], 100)

            # 生成图像矩阵后给Ui线程发消息，绘制当前图像
            ProjectForm.global_signal.update_text.emit(
                "已完成图像处理")
            ProjectForm.global_signal.draw_picture.emit(self.customFig, geo_graphic.current_data)

        self.__to_canvas()  # 切换到画布界面
        # 解析当前MethodList，运行方法
        self.show_message("方法开始运行...")
        # 建立GeoGraphicProcess对象
        self.single_result = GeoGraphicProcess()

        # 启动一个线程去完成打开读取图片的操作
        thread = threading.Thread(target=thread_run_methods, args=(self.method_list, self.single_result))
        thread.start()

    def run_method_single(self):
        """
        根据当前method_list的内容进行解析并运行方法，单步运行
        :return:
        """

        def thread_run_preprocess(method_list: MethodList, geo_graphic: GeoGraphicProcess):
            """
            线程体1，负责解析和执行MethodList的遥感影像读取和预处理操作
            :param method_list: MethodList对象
            :param geo_graphic: 传入的GeoGraphicProcess对象，保存了单步执行的中间结果
            :return:
            """
            # 1. 读取图像
            geo_graphic.read_file(method_list.project_data["input"]["path"])
            # 2. 根据MethodList设置的波段或者指数计算方法，生成初步的图像矩阵
            ProjectForm.global_signal.update_text.emit(
                "读取遥感影像 {}".format(method_list.project_data["input"]["path"]))
            geo_graphic.set_input_graphic(method_list.project_data["input"])

            # 3. 根据MethodList设置的预处理方法，进行预处理，得到数据清洗后的图像矩阵
            ProjectForm.global_signal.update_text.emit("执行预处理流程")
            geo_graphic.run_preprocess(method_list.project_data["preprocess"])

            # 生成图像矩阵后给Ui线程发消息，绘制当前图像
            ProjectForm.global_signal.update_text.emit("已完成图像处理")
            ProjectForm.global_signal.draw_picture.emit(self.customFig, geo_graphic.current_data)

        def thread_run_method(method_list: MethodList, geo_graphic: GeoGraphicProcess, method_index: int):
            """
            线程体2，负责解析和执行MethodList的遥感影像图像处理方法的某一步
            :param method_list: MethodList对象
            :param geo_graphic: 传入的GeoGraphicProcess对象，保存了单步执行的中间结果
            :param method_index: 欲执行方法的下标
            :return:
            """
            # 根据MethodList中的methods方法列表，处理图像数据
            method_dic = method_list.project_data["methods"][method_index]  # 获取这一步进行处理的方法字典
            # 执行单步图像处理操作
            geo_graphic.run_method_single(method_dic, method_list.project_data["method_num"], method_index)
            # 生成图像矩阵后给Ui线程发消息，绘制当前图像
            ProjectForm.global_signal.update_text.emit("已完成当前步骤操作")
            # 如果执行到最后一步了，再发一条消息
            if method_index == method_list.project_data["method_num"] - 1:
                ProjectForm.global_signal.update_text.emit("已执行到最后一步，完成图像处理")
            ProjectForm.global_signal.draw_picture.emit(self.customFig, geo_graphic.current_data)

        if self.single_step == -1:  # 如果未进行读取和预处理
            self.single_flag = True  # 由于选择了“单步执行”，把标志置位
            self.__to_canvas()  # 切换到画布界面，此时不会禁用“单步执行”按钮
            # 在线程外创建一个对象，传入线程内处理
            self.single_result = GeoGraphicProcess()
            self.show_message("单步运行方法开始...")

            self.btnSingleStepRun.setEnabled(False)  # 暂时屏蔽“单步运行”按钮
            # 启动一个线程去完成打开读取图片的操作
            thread = threading.Thread(target=thread_run_preprocess, args=(self.method_list, self.single_result))
            thread.start()
            self.single_step += 1
        else:
            self.__to_canvas()  # 如果不在画布界面，切换到画布界面

            self.btnSingleStepRun.setEnabled(False)  # 暂时屏蔽“单步运行”按钮
            # 启动一个线程去完成执行单步操作的操作
            thread = threading.Thread(target=thread_run_method, args=(self.method_list,
                                                                      self.single_result,
                                                                      self.single_step))
            thread.start()
            self.single_step += 1
            if self.single_step == self.method_list.project_data["method_num"]:  # 如果已经执行到了最后一部
                self.btnSingleStepRun.setEnabled(False)  # 禁用“单步执行”按钮
                self.single_flag = False  # 复位单步执行标志
                self.single_step = -1  # 复位单步执行步骤

    def __to_canvas(self):
        """
        私有方法，切换rojectForm的可变Widget到画布的界面
        :return:
        """
        if self.replaceableWidget.currentIndex() == 1:
            return

        self.replaceableWidget.setCurrentIndex(1)  # 切换到画布界面
        self.btnRun.setEnabled(False)  # 禁用“运行”按钮
        if not self.single_flag:  # 如果是单步执行的话，不能禁用“单步执行”按钮
            self.btnSingleStepRun.setEnabled(False)  # 禁用“单步运行”按钮
        self.functionGroupBox.setTitle("运行结果预览")
        self.btnAddMethod.setEnabled(False)  # 禁用用于编辑方法的4个按钮
        self.btnPreProcess.setEnabled(False)
        self.btnSetInput.setEnabled(False)
        self.btnSetOutput.setEnabled(False)
        self.btnCalculate.show()  # 显示“精度计算”和“返回方法编辑”按钮
        self.btnBackMethodList.show()

    def __back_method_list(self):
        """
        私有方法，切换ProjectForm的可变Widget到编辑方法的界面
        :return:
        """
        if self.replaceableWidget.currentIndex() == 0:
            return

        self.replaceableWidget.setCurrentIndex(0)  # 切换到方法编辑界面
        self.btnRun.setEnabled(True)  # 解锁“运行按钮”
        self.btnSingleStepRun.setEnabled(True)  # 解锁“单步运行按钮”
        self.functionGroupBox.setTitle("当前方法摘要")
        self.btnAddMethod.setEnabled(True)  # 解锁用于编辑方法的4个按钮
        self.btnPreProcess.setEnabled(True)
        self.btnSetInput.setEnabled(True)
        self.btnSetOutput.setEnabled(True)
        self.btnCalculate.hide()  # 隐藏显示“精度计算”和“返回方法编辑”按钮
        self.btnBackMethodList.hide()

    def show_picture(self, canvas: CustomFigureCanvas, array: np.ndarray):
        """
        自定义信号draw_picture的槽函数，属于Ui线程，根据传入图片矩阵信息更新控件
        :param canvas: 控件对象
        :param array: 图片矩阵
        :return:
        """
        canvas.show_picture(array)
        # canvas.show_hist(array)
        self.btnCalculate.setEnabled(True)  # 显示了图片，说明已经current_data已经存在，可以计算精度
        if self.single_flag:  # 如果现在是单步执行状态
            self.btnSingleStepRun.setEnabled(True)  # 解锁“单步执行”按钮

    def show_status(self, message: str):
        """
        自定义信号update_text的槽函数，属于Ui线程，根据传入字符串信息，添加到末尾
        :param message: 字符串信息
        :return:
        """
        self.txtConsole.append(message)
        self.txtConsole.ensureCursorVisible()  # 保持光标一直可见，即保持在最后一行
